package net.ojbk.twobox.client;

import net.ojbk.twobox.pojo.ClientInfo;
import net.ojbk.twobox.pojo.TcpMessage;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;
import java.util.Scanner;

/**
 * PACKAGE_NAME: net.ojbk.twobox.client
 * Description:
 * 连接用户流程：(新) 因为:tcp socket 可以直接互相连接，不需要一方使用serverSocket
 * 1、A从中央获取有那些用户，并且获取常驻通讯
 * 2、A新建一个Socket与Center建立连接，并告诉中央要连接B，通过B的UUID
 * 3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：uuid,ip,port,需要与你建立连接。
 * 4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
 * 5、中央记录B的ip,port。通知A：“B联系上了，它的ip\port，请做好准备”。通知B：“A已通知，请连接它”(通知完后，Center也要关闭与他们的连接)
 * 6、A开启线程直接连接B
 * 7、B开启线程直接连接B
 * 8、成功！
 */
public class Client { // 定位：Java客户端的应用程序窗口
    // 下面两个属性，相当于配置文件中一个properties的地位
    //public static String centerServerIp = "192.168.0.101";
    public static String centerServerIp = "ojbk.net";
    public static int centerServerPort = 9999;
    private static Logger logger = Logger.getLogger(Client.class);

    public static void main(String[] args) throws IOException {
        TCOConnectWithCenter clientWithCenter = new TCOConnectWithCenter(centerServerIp, centerServerPort);
        new Thread(clientWithCenter).start();
        logger.debug("启动线程：接收并处理CenterServer发来的信息。");

        String requestCode = "客户端可用操作：\n" +
                "1 获取所有clientInfo\n" +
                "2 与某打洞";
        System.out.print(requestCode);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            int code = scanner.nextInt();
            logger.debug("用户操作：输入操作码：" + requestCode);
            clientWithCenter.request(code);
        }
    }
}

/**
 * 定位：客户端与CenterServer进行TCP长连接的线程
 * 用于接收与处理 Center发过来的信息
 */
class TCOConnectWithCenter implements Runnable {
    private static Logger logger = Logger.getLogger(TCOConnectWithCenter.class);
    Socket socket;
    ObjectOutputStream objectOutputStream;
    ObjectInputStream objectInputStream;

    public TCOConnectWithCenter(String centerServerIp, int centerServerPort) throws IOException {
        this.socket = new Socket(centerServerIp, centerServerPort);
        logger.info("建立连接：与CenterServer建立TCP连接。本地Port:" + socket.getLocalPort());
        this.objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
        this.objectInputStream = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));
    }

    public void request(int type, Object... objs) throws IOException { // 用户操作，与CenterServer功能性通讯的入口
        TcpMessage tcpMessage = new TcpMessage();
        switch (type) {
            case 1:
                logger.debug("用户操作：请求获取服务器所有Client。");
                tcpMessage.code = 1;
                this.objectOutputStream.writeObject(tcpMessage);
                this.objectOutputStream.flush();
                logger.debug("发送消息：给CenterServer:" + tcpMessage);
                break;
            case 2:
                Scanner scanner = new Scanner(System.in);
                System.out.print("请输出目标UUID:");
                String uuid = scanner.next();
                System.out.println("收到：" + uuid);
                // A新建一个Socket与Center建立连接，(在新建的连接里面)并告诉中央要连接B，通过B的UUID
                TCPConnectWithBreakNat AClient = new TCPConnectWithBreakNat(uuid);
                new Thread(AClient).start();
                break;
        }
    }

    @Override
    public void run() { // 线程处理接受到的信息
        while (true) {
            try {
                // 如果想要加快速度，当然可以将处理信息单独放到线程里面。即每收到一个信息，就开一条线程处理（处理很快，所有不用担心线程多的爆掉）。
                // 但是需要考虑一些异步问题，这里我就单线程顺序处理，除非项目非常大，交流非常平凡，那么这里普通的已经足够了！
                TcpMessage tcpMessage = (TcpMessage) objectInputStream.readObject();
                logger.debug("收到信息：CenterServer:'" + tcpMessage + "'");
                handleMsgFromCenter(tcpMessage);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public void handleMsgFromCenter(TcpMessage tcpMessage) throws IOException { // 服务器操作：处理服务器发过来的信息
        /*
        1001 返回当前所有已连接客户的信息
        1002 有人请求TCP与我连接
         */
        switch (tcpMessage.code) {
            case 1001: // 1001 返回当前所有已连接客户的信息
                ClientInfo[] clientInfos = (ClientInfo[]) tcpMessage.data;
                for (ClientInfo clientInfo : clientInfos) {
                    System.out.println(clientInfo);
                }
                break;
            case 1002: // 有人（A）请求TCP与我连接
                // 接收A的外部uuid/ip/port/
                List data = (List) tcpMessage.data;
                // B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                new Thread(new TCPConnectWithBreakNat((String) data.get(0), (String) data.get(1), (int) data.get(2))).start();
                break;
        }
    }

}

/**
 * 定位：Client与Client建立连接部分，即p2p之间的TCP连接
 */
class TCPConnectWithBreakNat implements Runnable {
    // 预打洞TCP外部信息
    private String targetUUID;
    private String targetIp;
    private int targetPort;

    // 与对方通讯的 Socket（先是中央，后是P2P对等方）
    private Socket socket;
    // 与对方通讯的 in out流（先是中央，后是P2P对等方）
    private ObjectInputStream objectInputStream;
    private ObjectOutputStream objectOutputStream;

    // 其他信息
    private int type = 0; // 1 A 2 B
    private int localPort; // 非常重要，保证socket或Server的NAT内端口不变，那么NAT外端口不变
    private int outsidePort; // 同NAT下打洞比较重要！需要知道B（右边）
    private Logger logger = Logger.getLogger(TCPConnectWithBreakNat.class);
    // 这些东西啊
    private int requestConnectFrequency = -1;
    private boolean isOK = true;

    // B 接收者将会调用这个构造方法
    public TCPConnectWithBreakNat(String AUUID, String AIp, int APort) throws IOException {
        type = 2;
        logger.debug("P2P端-同意端：调用构造方法，发起者：" + AIp + ":" + APort + " " + AUUID);
        targetUUID = AUUID;
        targetIp = AIp;
        targetPort = APort;
        // 连接主机,自动更新 输入输出流引用
        toConnect(Client.centerServerIp, Client.centerServerPort);
        logger.debug("P2P端-同意端：连接CenterServer");
        // 4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
        // TODO 优化：中央返回的时候也返回 B的外网IP（2020.4.4 19.40 开始！）
        TcpMessage tcpMessage = new TcpMessage();
        tcpMessage.code = 3;
        tcpMessage.data = AUUID;
        objectOutputStream.writeObject(tcpMessage);
        objectOutputStream.flush();
        logger.info("P2P端-同意端：发送信息给中央：" + tcpMessage);
    }

    // A 发起者将会调用这个构造方法
    public TCPConnectWithBreakNat(String BUUID) throws IOException {
        type = 1;
        logger.debug("P2P端-发起端：调用构造方法，目标：" + BUUID);
        // 目标B UUID
        this.targetUUID = BUUID;
        // 连接主机,自动更新 输入输出流引用
        toConnect(Client.centerServerIp, Client.centerServerPort);
        logger.debug("P2P端-发起端：连接CenterServer");
        // 告诉中央，我要连接B
        TcpMessage tcpMessage = new TcpMessage();
        tcpMessage.code = 2;
        tcpMessage.data = BUUID;
        objectOutputStream.writeObject(tcpMessage);
        objectOutputStream.flush();
        logger.info("P2P端-发起端：发送信息给中央：" + tcpMessage);
    }

    /**
     * 连接服务器用的，本地随机一个端口就可以了，一定需要连接上，所以不需要设置短的超时时间
     */
    public void toConnect(String ip, int port) throws IOException {
        // 3s 默认连接超时时间
        this.toConnect(ip, port, 3000, -1);
    }

    /**
     * 连接P2P对等方用的，需要用以前的端口
     */
    public void toConnect(String ip, int port, int timeOut, int localPort) throws IOException {
        logger.info("P2P端" + (type == 1 ? "A" : "B") + "：开始发起向对方对等方的连接（" + ip + ":" + port + ")的连接！");
        socket = new Socket();
        socket.setReuseAddress(true);
        if (localPort != -1) { // 连接P2P对等方用的，需要用以前的端口，用于区分上面那个重载方法
            socket.bind(new InetSocketAddress(localPort));
            logger.debug("P2P端" + (type == 1 ? "A" : "B") + "：" + (type == 1 ? "A" : "B") + "绑定本地端口：" + localPort);
        }
        socket.connect(new InetSocketAddress(ip, port), timeOut);
        isOK = true;
        logger.info("P2P端" + (type == 1 ? "A" : "B") + "：建立了连接！！：" + ip + ":" + port);
        // socket.getOutputStream() 放到线程中，哈哈哈，自己琢磨，我就不解释了。
        if (localPort == -1) {
            objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectInputStream = new ObjectInputStream(socket.getInputStream());
        } else {
            if (type == 1) {
                objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectInputStream = new ObjectInputStream(socket.getInputStream());
            } else {
                objectInputStream = new ObjectInputStream(socket.getInputStream());
                objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            }

        }
        this.localPort = socket.getLocalPort();
        //while (true) {
        //    try {
        //        Thread.sleep(300);
        //    } catch (InterruptedException e) {
        //        logger.error("P2P端：" + e.getMessage());
        //    }
        //    if (objectOutputStream != null && objectInputStream != null)
        //        break;
        //}
    }

    /**
     * 关闭连接用
     */
    public void toClose() throws IOException {
        isOK = false;
        //objectOutputStream.close();
        //objectInputStream.close();
        socket.close();
        logger.info("P2P端：关闭连接");
    }

    @Override
    public void run() { // 处理收到的消息，连接对等方前是 Center发来信息，之后是对等方发来信息
        while (isOK) {
            TcpMessage tcpMessage = null;
            try {
                tcpMessage = (TcpMessage) objectInputStream.readObject();
                logger.debug("P2P端" + (type == 1 ? "A" : "B") + "：收到[" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "]信息：" + tcpMessage);
                // 开启处理
                handleMsg(tcpMessage);
            } catch (InterruptedException | IOException | ClassNotFoundException e) {
                logger.info("P2P端" + (type == 1 ? "A" : "B") + "：对面对等方断开连接！");
                if (!isOK) {
                    try {
                        toClose();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                        break;
                    }
                    break;
                }

            }
        }
    }

    /**
     * 处理收到的信息
     * 三、对等方(欲建立p2p连接的Socket)收到 的信息
     * 2001 Center通知A："我联系上B了，现在给你B的IP/Port，你可以连接B了。"
     * 2002 Center通知B："A已知道你准备好了，你可以连接A了"
     * 2003 一条文字消息
     */
    public void handleMsg(TcpMessage tcpMessage) throws IOException, InterruptedException, ClassNotFoundException {
        switch (tcpMessage.code) {
            case 2001: // Center通知A："我联系上B了，现在给你B的IP/Port，你可以连接B了。
                Object[] dataArray = (Object[]) tcpMessage.data;
                targetIp = (String) dataArray[0];
                targetPort = (int) dataArray[1];
                // 关闭以前连接(两头都不关闭也可以，比如做一个是否成功的回访，啥的。)
                toClose();
                // 向对等方建立p2p连接
                toCreateP2PConnect();
                // 发个信息快乐下吧！
                TcpMessage toB = new TcpMessage();
                toB.code = 2003;
                toB.msg = "1、我是A，很高兴见到你！";
                objectOutputStream.writeObject(toB);
                objectOutputStream.flush();
                // 回复一条消息
                OutputStream outputStream = socket.getOutputStream();
                byte[] bytes = new byte[1024];
                int len = -1;
                File file = new File("C:\\Users\\Administrator\\Desktop\\IMG_0473.JPG");
                FileInputStream fileInputStream = new FileInputStream(file);
                while ((len = fileInputStream.read(bytes)) != -1) {
                    TcpMessage fileMsg = new TcpMessage();
                    fileMsg.code = 2004;//不用
                    fileMsg.msg = "一个文件：" + file.getName();
                    Object[] objects = {bytes, len};
                    fileMsg.data = objects;
                    objectOutputStream.writeObject(fileMsg);
                    objectOutputStream.flush();
                }
                fileInputStream.close();
                TcpMessage fileEndMsg = new TcpMessage();
                fileEndMsg.code = 2004;
                fileEndMsg.msg = "传输完成";
                Object[] objects = {file.getName(), -1};
                fileEndMsg.data = objects;
                objectOutputStream.writeObject(fileEndMsg);
                objectOutputStream.flush();
                break;
            case 2002: // 2002 Center通知B："A已知道你准备好了，你可以连接A了"，附带了B自己的外网IP
                toClose(); // 先关闭原来Socket
                outsidePort = (int) tcpMessage.data;
                toCreateP2PConnect();
                // 发个信息快乐下吧！
                TcpMessage toA = new TcpMessage();
                toA.code = 2003;
                toA.msg = "我是B，啊啊啊啊啊啊啊啊啊啊啊啊，开心噢！";
                objectOutputStream.writeObject(toA);
                objectOutputStream.flush();
                // 接收一个文件
                int fileBoxLen = -1;
                File tempFile = new File("tempFile");
                FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
                TcpMessage tempMsg = (TcpMessage) objectInputStream.readObject();
                Object[] data = (Object[]) tempMsg.data;
                while ((fileBoxLen = (int) data[1]) != -1) {
                    fileOutputStream.write((byte[]) data[0], 0, fileBoxLen);
                    tempMsg = (TcpMessage) objectInputStream.readObject();
                    data = (Object[]) tempMsg.data;
                }
                fileOutputStream.flush();
                fileOutputStream.close();
                tempFile.renameTo(new File((String)data[0]));
                break;
            case 2003: // 一条信息
                System.out.println("收到信息：" + tcpMessage.msg);
                break;
        }
    }

    /**
     * 向对等方建立p2p连接
     * 2001 2002 的后续操作一样，其实可以在CenterServer 回复指令的时候就设置成一个，就不用单独提出这个方法了。这里就不改了。
     */
    public void toCreateP2PConnect() {
        for (int i = 0; i < 5; i++) {
            try {
                //toConnect(targetIp, targetPort, 500, this.localPort);
                logger.debug((type == 1 ? "A" : "B") + "：对面原本端口：" + targetPort);
                //if (type == 1) { // A 左边那个
                //    toConnect(targetIp, targetPort + 2, 200, this.localPort);
                //
                //} else { // B 右边那个,他要链接自己+1
                //    toConnect(targetIp, outsidePort + 1, 200, this.localPort);
                //}
                toConnect(targetIp, targetPort + 1, 200, this.localPort);
                // 到了这里就是连接成功
                logger.info("P2P端：成功突破TCP NAT，成功建立连接！");
                requestConnectFrequency = i + 1;
                break;
            } catch (IOException e) {
                logger.debug("P2P端：" + (type == 1 ? "A" : "B") + "发起第" + ++i + "次向对等方（" + targetIp + ":" + targetPort + "的连接！");
            }
        }

    }
}